/**
 * This software was produced for the U. S. Government
 * under Contract No. FA8721-09-C-0002, and is
 * subject to the Rights in Noncommercial Computer Software
 * and Noncommercial Computer Software Documentation Clause
 * (DFARS) 252.227-7014 (JUN 1995)
 * 
 * (c) 2009 The MITRE Corporation. All Rights Reserved.
 */
package com.mitre.spmif2opnet.generation;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.mitre.spmif2opnet.VisitorContext;
import com.mitre.spmif2opnet.analysis.AnalysisContext;
import com.mitre.spmif2opnet.analysis.AnalysisException;
import com.mitre.spmif2opnet.analysis.Identifier;
import com.mitre.spmif2opnet.analysis.ScenarioPartitions;
import com.mitre.spmif2opnet.analysis.ast.Device;
import com.mitre.spmif2opnet.analysis.ast.Graph;
import com.mitre.spmif2opnet.analysis.ast.Node;
import com.mitre.spmif2opnet.analysis.ast.Project;
import com.mitre.spmif2opnet.analysis.ast.Scenario;
import com.mitre.spmif2opnet.generation.ir.Endpoint;
import com.mitre.spmif2opnet.generation.ir.Job;
import com.mitre.spmif2opnet.generation.ir.Link;
import com.mitre.spmif2opnet.generation.ir.Phase;
import com.mitre.spmif2opnet.generation.ir.PhysicalNode;
import com.mitre.spmif2opnet.generation.ir.Task;
import com.mitre.spmif2opnet.opnet.network.Attr;
import com.mitre.spmif2opnet.opnet.network.Network;
import com.mitre.spmif2opnet.opnet.network.Subnet;

/**
 * The primary S-PMIF abstract state to OPNET conversion routine.
 * NOTE: This code is rough because we have changed the specifications for the translation
 *  a number of times.
 */
public class OpnetGraphConverter implements GraphConverter {

	/**
	 *  Our current path
	 *  - consumer is the main loop
	 *  - producer is the successors to node put in the context "curr node" after the loop
	 */
	private LinkedList<Node> _curr_path;
	/**
	 *  Holds the tasks and jobs we generate
	 */
	private List<Task>  _tasks;
	private List<Job>   _jobs;
	/**
	 *  And the servers and links - key - device name
	 */
	private Hashtable<String, PhysicalNode>  _servers;
	private List<Link>  _links;
	
	/**
	 * Default Constructor
	 */
	public OpnetGraphConverter() {
		_curr_path = null;
		_tasks = new LinkedList<Task>();
		_jobs = new LinkedList<Job>();
		_servers = new Hashtable<String, PhysicalNode>();
		_links = new LinkedList<Link>();
	}
	
	/**
	 * Convert the AST into the OPNET IR
	 * @param c the analysis context, with the AST
	 * @throws GenerationException
	 */
	public void convert(AnalysisContext c) throws GenerationException {
		// Calculate the scenario priorities
		try {
			c.calculatePriorities();
		} catch (AnalysisException exc) {
			throw new GenerationException(exc.getMessage());
		}
		
		ScenarioPartitions parts = c.getPartitions();
		for (ScenarioPartitions.Partition part : parts.getPartitions()) {
			System.out.println("[INFO] Scenarios partitioned into thread = " + part.toString());
		}
		
		// Go through the scenario partitions, creating tasks
		for (ScenarioPartitions.Partition part : c.getPartitions().getPartitions()) {
			// We only need to worry about the root of the partition 
			//  every other scenario will be deal with in the meantime
			// Validation assigns the root!
			Identifier root = part.getRoot();
			Project p = c.getProject(part.getRoot().getProjectName());
				// Get the top level graph in the scenario
				try {
					Scenario s = p.getScenario(root);
					// Initialize
					Graph g = s.getStartGraph();
					Task t = new Task(s.getName() + "_" + g.getId(), s.getInterarrivalTime());
					GenerationContext gc = new GenerationContext(this, c.getPriorities());
					_curr_path = new LinkedList<Node>();
					_curr_path.add(g.getStartNode());
					ProcNetworkFSM fsm = new ProcNetworkFSM();
					// Record the project devices we've missed
					Hashtable<String, PhysicalNode> devs = p.getOverhead().createDevices();
					for (String pn : devs.keySet()) {
						if (!_servers.containsKey(pn)) {
							_servers.put(pn, devs.get(pn));
						}
					}
					
					// Set up the context
					gc.setCurrProject(p);
					gc.setCurrGraphId(g.getIdentifier());
					gc.firstTaskNotDone();
					gc.clearNetworkPhase();

					while (!_curr_path.isEmpty()) {
						// Put the current node in the context
						gc.setCurrNode(this.peekTopNode());
						// Set the source device - the first one
						if (!gc.isFirstTaskDone()) {
							String source_dev = gc.getDeviceId(gc.getResourceId(Device.DeviceCategory.DEVICE_PROCESSING), 
									Device.DeviceCategory.DEVICE_PROCESSING);
							if (source_dev == null) {
								System.out.println("Unable to find starting resource for root!  Exiting...");
								System.exit(-1);
							}
							_servers.get(source_dev).setAsSource();
							t.setSource(source_dev);
						}
						
						// Get the name of our resource, and thus our logical node
						String res = gc.getResourceId(Device.DeviceCategory.DEVICE_PROCESSING);
						boolean diff_resource = gc.endpointChanged(res);
						ProcNetworkFSM.Letter l =
							ProcNetworkFSM.parseLetter(gc.getCurrNode(), p.getOverhead(), diff_resource);
						// Let the FSM chew on it
						fsm.process(l, gc);
						// Add successors to the worklist
						_curr_path.addAll(g.getSuccessors(gc.getCurrNode()));
						// Remove the node from the work queue
						this.consumeTopNode();
					}

					// Clean out the context
					for (Phase phase : gc.getPhases()) {
						t.addPhase(phase);
					}
					for (Job j : gc.getJobs()) {
						_jobs.add(j);
					}					
					for (Link l : gc.getLinks()) {
						// Bad - should unify with code with the gen context link check code
					if (!_links.contains(l) && !_links.contains(l.reverseLink())) {
							_links.add(l);
						}
					}
				
					// Store the task
					_tasks.add(t);
					
					// For debugging
//					for (PhysicalNode pn : _servers.values()) {
//						System.out.println("Server: " + pn.getId()+ " is source? " + pn.isSource());
//					}
					
				} catch (AnalysisException exc) {
					throw new GenerationException(exc.getMessage());
				}
			}
	}
	
	// *** OPNET Network Generation Code 			

	/**
	 * Export the tasks to XML
	 * @param gen_network should we generate the OPNET network topology?
	 * @return the OPNET network
	 */
	public Network exportTasks(boolean gen_network) {
		// Step #1 - create the simple shell network and subnet
		Network network = new Network();
		network.setAttributeProcessing("explicit");
		network.setVersion("1.6"); // arbitrary - from external dump
		network.setLocale("C");
		// NOTE: network name?
		Subnet subnet = new Subnet();
		subnet.setMobility("fixed");
		subnet.setName("Logical Network");
		
		// Step #2 - create the task, appplication, and profile config nodes
		// Task
		com.mitre.spmif2opnet.opnet.network.Node tasks = new com.mitre.spmif2opnet.opnet.network.Node();
		tasks.setName("Tasks");
		tasks.setIgnoreQuestions("true");
		tasks.setModel("Task Config");
		tasks.setMinMatchScore("strict matching");
		tasks.setMobility("fixed");
		
		// Application
		com.mitre.spmif2opnet.opnet.network.Node apps = new com.mitre.spmif2opnet.opnet.network.Node();
		apps.setName("Apps");
		apps.setIgnoreQuestions("true");
		apps.setModel("Application Config");
		apps.setMinMatchScore("strict matching");
		apps.setMobility("fixed");

		// Profile
		com.mitre.spmif2opnet.opnet.network.Node profs = new com.mitre.spmif2opnet.opnet.network.Node();
		profs.setName("Profiles");
		profs.setIgnoreQuestions("true");
		profs.setModel("Profile Config");
		profs.setMinMatchScore("strict matching");
		profs.setMobility("fixed");
		
		// Init tasks, apps, and profs
		Attr a = makeAttr("Task Specification.count",  Integer.toString(_tasks.size()));
		tasks.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().add(a);

		a = makeAttr("Application Definitions.count", Integer.toString(_tasks.size()));
		apps.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().add(a);

		Hashtable<String, Set<String>> prof_names = new Hashtable<String, Set<String>>();		
		int i = 0;
		// NOTE: To simplify application processing, we cache the app names to use in the server/job processing
		//  A more refined implementation may want to constrain the entries to the specific appls that
		//  use specific jobs - this would have to be done during translation, however
		Set<String> app_names = new HashSet<String>();
		for (Task t : _tasks) {
			// One Profile per app (and thus task)
			String prof_name = "prof" + i;
			String prefix = "Profile Configuration[" + i + "].";

			profs.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().add(makeAttr("Profile Configuration.count", Integer.toString(_tasks.size()), "true"));
			profs.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(this.getProfInitAttributes(prefix, 1, prof_name));
			Set<String> prof_set;
			if (prof_names.containsKey(t.getSource())) {
				prof_set = prof_names.get(t.getSource());
			} else {
				prof_set = new HashSet<String>();
			}
			prof_set.add(prof_name);
			prof_names.put(t.getSource(), prof_set);
			
			// One app per task
			prefix = "Application Definitions [" + i + "].";
			String task_name = t.getName();
			String app_name  = "app" + task_name;
			apps.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(getAppAttributes(prefix, task_name, app_name));
			// One profile per app
			prefix = "Profile Configuration[" + i + "].Applications [0].";
			profs.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(getProfAttributes(prefix, app_name, t.getInterarrivalTime()));
			// Tasks
			prefix = "Task Specification [" + i + "].";
			tasks.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(t.getAttributes(prefix));
			++i;
			app_names.add(app_name);
		}
		subnet.getNodeOrBus().add(profs);
		subnet.getNodeOrBus().add(apps);
		subnet.getNodeOrBus().add(tasks);
		
		// Step #3 - create the physical network from devices
		Hashtable<String, com.mitre.spmif2opnet.opnet.network.Node> devices =
			new Hashtable<String, com.mitre.spmif2opnet.opnet.network.Node>();
		Hashtable<String, Set<Job>> dev_jobs = new Hashtable<String, Set<Job>>();
		if (gen_network) {
			for (PhysicalNode pnode : _servers.values()) {
				String pname = pnode.getId();
				// Load up the apps
				devices.put(pname, getServerAttributes(pnode, app_names, prof_names.get(pname)));
				dev_jobs.put(pname, new HashSet<Job>());
			}
		}
		
		// Step #4 - create the server config from jobs
		com.mitre.spmif2opnet.opnet.network.Node servers = new com.mitre.spmif2opnet.opnet.network.Node();
		servers.setName("Servers");
		servers.setIgnoreQuestions("true");
		servers.setModel("Server_Config");
		servers.setMinMatchScore("strict matching");
		servers.setMobility("fixed");
		
		a = makeAttr("Job Definitions.count", Integer.toString(_jobs.size()));
		servers.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().add(a);
		i = 0;
		for (Job j : _jobs) {
			// Install the jobs in the Server Config
			String prefix = "Job Definitions [" + i + "]."; 
			servers.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(j.getAttributes(prefix));
			// Install the jobs in the actual servers, maintaining job counts
			if (gen_network) {
				String name = j.getDeviceName();
				Set<Job> djobs = dev_jobs.get(name);
				djobs.add(j);
				dev_jobs.put(name, djobs);
			}
			i++;
		}
				
		// Put the final job counts in the servers
		if (gen_network) {
			for (String dev_name : devices.keySet()) {
				com.mitre.spmif2opnet.opnet.network.Node s = devices.get(dev_name);			
				int num_jobs = dev_jobs.get(dev_name).size();
				a = makeAttr("Server: Advanced Server Configuration [0].Job Definitions.count",
							Integer.toString(num_jobs).toString());
				s.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().add(a);
				
				i = 0;
				for (Job j : dev_jobs.get(dev_name)) {
					String prefix = "Server: Advanced Server Configuration [0].Job Definitions [" + i + "].";
					s.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(j.getServerAttributes(prefix));
					++i;
				}
				devices.put(dev_name, s);
			}
		}
		
		// Add the server config
		subnet.getNodeOrBus().add(servers);
		
		// Add the physical devices
		if (gen_network) {
			subnet.getNodeOrBus().addAll(devices.values());
			LinkAllocator lalloc = new LinkAllocator();
			lalloc.AllocateLinks(_links);
			for (Link l : _links) {
				subnet.getLinkOrTap().add(l.makeLink());
			}
		}
		
		network.getSubnet().add(subnet);
		return network;
	}
		
	/**
	 * Generate OPNET server attributes
	 * @param pnode the physical node
	 * @param app_names OPNET application names
	 * @param prof_names OPNET profile names
	 * @return OPNET XML
	 */
	private com.mitre.spmif2opnet.opnet.network.Node getServerAttributes(PhysicalNode pnode, 
			Set<String> app_names, Set<String> prof_names) {
		com.mitre.spmif2opnet.opnet.network.Node n = new com.mitre.spmif2opnet.opnet.network.Node();
		n.setName(pnode.getId());
		n.setModel(pnode.getModel());
		n.setIgnoreQuestions("true");
		n.setMinMatchScore("strict_matching");
		n.setMobility("fixed");
		n.getCharacteristicOrAttrOrExtAttrOrPstateOrUiStatus().addAll(pnode.getAttributes(app_names, prof_names));
		return n;
	}
	
	/**		
	 * Generate OPNET application attributes
	 * @param prefix string prefix
	 * @param task_name which task
	 * @param app_name which application
	 * @return the list
	 */
	private List<Attr> getAppAttributes(String prefix, String task_name, String app_name) {
		List<Attr> attrs = new LinkedList<Attr>();
		attrs.add(makeAttr(prefix + "Name", app_name));
		attrs.add(makeAttr(prefix + "Description.count", Integer.toString(1)));
		// NOTE: It may behoove us to make this part more flexible, but for now we just dump a single custom app
		attrs.add(makeAttr(prefix + "Description [0].Custom.count", "1"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Task Description.count", "1", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Task Description [0].Task Name", task_name));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Task Description [0].Task Weight", "10"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Task Ordering", "Serial (Ordered)"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Transport Protocol", "UDP", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Transport Port", "Default", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Type of Service", "Best Effort (0)", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].Connection Policy", "Refresh After Application", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].RSVP Parameters", "None", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Custom [0].ACE Task Interdependency", "Final Response Arrival at Client", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Database", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Email", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Ftp", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Http", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Print", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Remote Login", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Video Conferencing", "Off", "true"));  
		attrs.add(makeAttr(prefix + "Description [0].Voice", "Off", "true"));  		
		return attrs;
	}

	/**
	 * Generate OPNET profile initialization attributes
	 * @param prefix
	 * @param num_apps  the number of applications for the profile
	 * @param prof_name the profile name
	 * @return the list
	 */
	private List<Attr> getProfInitAttributes(String prefix, int num_apps, String prof_name) {
		List<Attr> attrs = new LinkedList<Attr>();
		attrs.add(makeAttr(prefix + "Profile Name", prof_name)); 
		attrs.add(makeAttr(prefix + "Applications.count", Integer.toString(num_apps),"true"));
		attrs.add(makeAttr(prefix + "Operation Mode", "Simultaneous","true")); 
		attrs.add(makeAttr(prefix + "Start Time", "constant (0)"));  
		attrs.add(makeAttr(prefix + "Duration", "End of Last Application","true")); 
		attrs.add(makeAttr(prefix + "Repeatability.count", "1")); 
		attrs.add(makeAttr(prefix + "Repeatability [0].Inter-repetition Time", "constant (0)")); 
		attrs.add(makeAttr(prefix + "Repeatability [0].Number of Repetitions", "Unlimited","true")); 
		attrs.add(makeAttr(prefix + "Repeatability [0].Repetition Pattern", "Serial","true")); 
		return attrs;
	}

	/**
	 * Generate OPNET profile attributes
	 * @param prefix
	 * @param app_name the application name on the profile
	 * @param inter_arrival_time the interarrival time
	 * @return the list
	 */
	private List<Attr> getProfAttributes(String prefix, String app_name, double inter_arrival_time) {
		List<Attr> attrs = new LinkedList<Attr>();		  
		attrs.add(makeAttr(prefix + "Name", app_name));
		attrs.add(makeAttr(prefix + "Start Time Offset", "No Offset", "true"));
		attrs.add(makeAttr(prefix + "Duration", "End of Last Task", "true"));
		attrs.add(makeAttr(prefix + "Repeatability.count", "1")); 
		attrs.add(makeAttr(prefix + "Repeatability [0].Inter-repetition Time", "constant(" 
				+ Double.toString(inter_arrival_time) + ")"));  
		attrs.add(makeAttr(prefix + "Repeatability [0].Repetition Pattern", "Concurrent", "true")); 
		return attrs;
	}
	
	/**
	 * Attribute from name, value
	 * @param name
	 * @param value
	 * @return
	 */
	public static Attr makeAttr(String name, String value) {
		Attr a = new Attr();
		a.setName(name);
		a.setValue(value);
		return a;
	}

	/**
	 * Attribute from name, value, is it symbolic?
	 * @param name
	 * @param value
	 * @param symb
	 * @return
	 */
	public static Attr makeAttr(String name, String value, String symb) {
		Attr a = new Attr();
		a.setName(name);
		a.setValue(value);
		a.setSymbolic(symb);
		return a;
	}
	
	/**
	 * Attribute from name, value, is it symbolic, is the value intended?
	 * @param name
	 * @param value
	 * @param symb
	 * @return
	 */
	public static Attr makeAttr(String name, String value, String symb, String intend) {
		Attr a = new Attr();
		a.setName(name);
		a.setValue(value);
		a.setSymbolic(symb);
		a.setIntended(intend);
		return a;
	}
	
	/**
	 * Dump the tasks to a user-readable string
	 */
	public void dumpTasks() {
		int i = 0;
		List<Attr> attrs = new LinkedList<Attr>();
		for (Task t: _tasks) {
			String prefix = "Task Specification [" + i + "]";
			attrs.addAll(t.getAttributes(prefix));
			++i;
		}
		
		for (Attr a : attrs) {
			System.out.println(a.toString());
		}
	}
	
	/**
	 * Look at the top node of the current path
	 * @return the node
	 * @throws GenerationException
	 */
	public Node peekTopNode() throws GenerationException {
		if (_curr_path.isEmpty()) {
			throw new GenerationException("Attempt to peek at S-PMIF node from empty path!");
		}
		
		return _curr_path.peek();
	}

	/** 
	 * Remove the top node from the path
	 * @throws GenerationException
	 */
	public void consumeTopNode() throws GenerationException {
		if (_curr_path.isEmpty()) {
			throw new GenerationException("Attempt to consume S-PMIF node from empty path!");
		}
		
		_curr_path.remove();
	}

	/**
	 * This gets the last used end-point - note that you will usually copy & modify
	 */
	public String getResource(GenerationContext c) {
		return c.getPrevResource().getName();
	}

	/**
	 *  This creates the end-point template based on the current S-PMIF node
	 *  at the head of the path
	 *  What you actually do with it depends on the application
	 */
	public String makeResource(GenerationContext c) throws GenerationException {
		return c.getResourceId(Device.DeviceCategory.DEVICE_PROCESSING);
	}
	
	// *** These are the callback functions from the FSM
	
	public GenerationContext genProcessingLoop(GenerationContext c, String s) {
		// Create the phase structure based on the node structure
		c.setCurrResource(s);
		VisitorContext<GenerationContext> vc 
			= c.getCurrNode().accept(new OpnetNodeVisitor(), new VisitorContext<GenerationContext>(c));
		return vc.getContext();
	}


	public GenerationContext genNetworking(GenerationContext c, String start, String end) throws GenerationException {
		c = genNetworkingStart(c, start);
		return genNetworkingEnd(c, end);
	}

	// Generate a networking phase with a start endpoint
	public GenerationContext genNetworkingStart(GenerationContext c, String start) throws GenerationException {
		Phase net_phase = new Phase(Phase.getNewPhaseId(), c.getPrevPhase());
		c.setPrevPhase(null);
		// We get from the input endpoint: 
		Endpoint my_start = new Endpoint(start);
		my_start.setPointDesignation(Endpoint.SpecialPoint.NAMED);
		my_start.setInitTime(c.getTiming(Device.DeviceCategory.DEVICE_NETWORK));			
			
		// NOTE (In general): We definitely need packet sizes - how? Different resource?
		// NOTE (In general): Jobs needed for networks?
		
		net_phase.setSource(my_start);
		c.setNetworkPhase(net_phase);
		return c;
	}

	public GenerationContext genNetworkingMiddle(GenerationContext c) throws GenerationException {
		Phase net_phase = c.getNetworkPhase();
		Endpoint start = net_phase.getSource();
		start.setInitTime(c.getTiming(Device.DeviceCategory.DEVICE_NETWORK) + start.getInitTime());
		net_phase.setSource(start);
		c.setNetworkPhase(net_phase);
		return c;
	}
	
	public GenerationContext genNetworkingEnd(GenerationContext c, String end) throws GenerationException {
		Phase net_phase = c.getNetworkPhase();
		Endpoint start = net_phase.getSource();
		start.setInitTime(c.getTiming(Device.DeviceCategory.DEVICE_NETWORK) + start.getInitTime());

		// Remember - in our OPNET performance modeling, the start endpoint (src->dest traffic)
		//  contains all of the pertinent info
		Endpoint my_end = new Endpoint(end);
		my_end.setPointDesignation(Endpoint.SpecialPoint.NAMED);
		my_end.setInitTime(0.0);			
		
		// Now the phase has two endpoints
		net_phase.setDestination(my_end);
		// The networking phase is complete - add it to the tally
		c.addPhase(net_phase);
		// Destroy the temp one
		c.setNetworkPhase(null);
		// Add the phase to the list of links!
		Link l = new Link(c.getDeviceId(start.getName(), Device.DeviceCategory.DEVICE_PROCESSING),
				c.getDeviceId(end, Device.DeviceCategory.DEVICE_PROCESSING));
		c.addLink(l);
		return c;
	}


	
}
